Skip to content

fix(thread-pool): report a bootloader failure through the worker's error stream - #225

Merged
EdmondDantes merged 1 commit into
mainfrom
fix/bootloader-failure-unreported
Aug 13, 2026
Merged

fix(thread-pool): report a bootloader failure through the worker's error stream#225
EdmondDantes merged 1 commit into
mainfrom
fix/bootloader-failure-unreported

Conversation

@EdmondDantes

@EdmondDantes EdmondDantes commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

A ThreadPool bootloader that throws had two ways out of the worker, and both are private to whoever submits tasks: the message is kept for the next submit(), and the tasks already queued are rejected with the exception. A pool driven as a set of long-lived workers reads neither. The HTTP server submits one internal task per worker and awaits it only for completion, so the exception was dropped and the process exited 0 with nothing on stdout, nothing in error_log, and no worker ever having served.

The worker now hands the exception to zend_exception_error() before it closes the pool. That is the same call the engine makes for an uncaught exception at the end of a request, so display_errors, log_errors, error_log and error_reporting all apply, and the output reads as usual:

PHP Fatal error:  Uncaught RuntimeException: boot failed! in /app/boot.php:12

zend_exception_error() adds E_DONT_BAIL, so the worker still reaches the close-and-reject path and every existing consumer sees what it saw before. set_exception_handler() is not invoked: the exception is reported, not dispatched.

The bailout branch (exit() in the bootloader, or a fatal error) keeps its silence — a fatal error has already printed itself on the way to the bailout, and exit() is not an error.

Tests

080-bootloader_exception_reported.phpt is new: a pool whose bootloader throws, with nothing submitted at all, must still produce the fatal. 063-bootloader_exception.phpt keeps its assertion that the message reaches the awaiter and now also expects the report.

Locally: ext/async, 1230 tests, 3 failures — curl/063, curl/064, io/082. None of them creates a ThreadPool, so this change cannot run in them.

…ror stream

A bootloader that threw was kept for a later submit() and delivered to the
awaiters of the tasks already queued, and nowhere else. A pool driven as a set
of long-lived workers reads neither channel: the HTTP server submits one
internal task per worker and awaits it only for completion, so the exception
was lost and the pool died with no diagnostic at all.

The worker now hands the exception to zend_exception_error() before it closes
the pool, which reports it the way any uncaught exception is reported —
display_errors, log_errors and error_log all apply. That call adds E_DONT_BAIL,
so the worker still reaches the close/reject path. The bailout branch keeps its
silence: a fatal error has already printed itself on the way there, and exit()
is not an error.
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes
EdmondDantes merged commit 3df835c into main Aug 13, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant